- This fantastic 2-in-1 laptop I tested is highly recommended for office workers (and it's on sale)
- This Eufy twin-turbine robot vacuum is a steal at $350 for Black Friday
- The robot vacuum that kept my floors free of muddy paw prints this fall is $600 off
- Here's how to get the ultimate Kindle bundle for $135 this Black Friday (plus more ways to mix and match deals)
- This racecar-looking robot mower mows a gorgeous lawn and is on sale for Black Friday
IaC: Azure Resource Manager Templates vs. Terraform
Infrastructure as code (IaC) is the process of configuring infrastructure through code instead of manually. A manual process requires operators and system administrators to configure any changes to the infrastructure.
Using IaC, DevOps teams can store the infrastructure configuration code and application code in a centralized repository. IaC ensures consistent and more secure deployment. By avoiding error-prone manual configuration and deployment, security standards and policies are easier to maintain. And, DevOps engineers can improve scalability and improve productivity through faster deployments.
DevOps engineers can choose from various tools when implementing IaC. Two of the most popular tools for Microsoft Azure are Azure Resource Manager (ARM) templates and HashiCorp Terraform. Proprietary offerings like ARM templates allow infrastructure configuration exclusively on their respective cloud providers. On the other hand, a vendor-neutral tool like HashiCorp’s Terraform supports multiple cloud providers.
This article reviews the differences between ARM templates and Terraform HCL files and discusses the pros and cons of these two options, helping you choose the right deployment solution for your project’s infrastructure.
ARM Templates
An ARM template is a JavaScript Object Notation (JSON) file that defines the configuration of a project’s infrastructure. ARM templates use a declarative syntax that states what DevOps engineers want to deploy, like virtual machines (VMs), storage systems, and other resources.
Below is a blank ARM template:
{
“$schema”: “https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#”,
{
“$schema”: “https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#”,
“contentVersion”: “1.0.0.0”,
“parameters”: {
},
“variables”: {
},
“functions”: [],
“resources”: [
],
“outputs”: {
}
}
The first section is the schema, which gives the location of the file that defines the structure of the JSON file. The other sections are:
- Parameters
- Variables
- Resources
- Functions
Parameters
This section provides deployment values. For each parameter, set the type and metadata, which explains the parameter. You can also set the defaultValue and allowedValues variables if they don’t acquire other values. Parameters make templates reusable in different deployment environments.
“vmName”: {
“defaultValue”: “simpleLinuxVM”,
“type”: “String”,
“metadata”: {
“description”: “The name of you Virtual Machine.”
}
}
The vmName parameter has a string type, with a defaultValue of simpleLinuxVM. The vmName parameter uses this value if you don’t specify a value. The vmName parameter also provides metadata with a description that explains what vmName is.
As a best practice for security, user names and passwords or other sensitive credentials should use secureString or secureObject types. Parameters with secureString or secureObjects cannot be read after the resource is deployed.
Variables
Variables are values that you define and reuse throughout the template. They can be integers, strings, or even objects. For instance, if you assign the variable name osDiskType the value Standard_LRS, then anytime you want to use that value, you can use its variable name.
“resources”: [
{
“type”: “Microsoft.Network/networkInterfaces”,
“apiVersion”: “2020-06-01”,
“name”: “[variables(‘networkInterfaceName’)]”,
“location”: “[parameters(‘location’)]”,
“dependsOn”: [
“[resourceId(‘Microsoft.Network/networkSecurityGroups’, parameters(‘networkSecurityGroupName’))]”,
“[resourceId(‘Microsoft.Network/publicIPAddresses’, variables(‘publicIPAddressName’))]”,
“[resourceId(‘Microsoft.Network/virtualNetworks/subnets’, parameters(‘virtualNetworkName’), parameters(‘subnetName’))]”
],
“properties”: {
“ipConfigurations”: [
{
“name”: “ipconfig1”,
“properties”: {
“subnet”: {
“id”: “[resourceId(‘Microsoft.Network/virtualNetworks/subnets’, parameters(‘virtualNetworkName’), parameters(‘subnetName’))]”
},
“privateIPAllocationMethod”: “Dynamic”,
“publicIPAddress”: {
“id”: “[resourceId(‘Microsoft.Network/publicIPAddresses’, variables(‘publicIPAddressName’))]”
}
}
}
],
“networkSecurityGroup”: {
“id”: “[resourceId(‘Microsoft.Network/networkSecurityGroups’, parameters(‘networkSecurityGroupName’))]”
}
}
},
]
This resource has five different parts:
- type is the service to deploy.
- apiVersion specifies the configurations available for that resource.
- name is the name of the resource.
- Location is the Azure region in which the resource group deploys.
- Properties provide the configuration options for the resource.
Functions
Functions are expressions that you define and call whenever you want to use them in your application
You can use the Azure CLI or PowerShell to deploy an ARM template. With PowerShell, use New-AzResourceGroupDeployment to specify the deployment name, the resource group name, and the path to the template file.
HashiCorp’s Terraform
Terraform is a cloud-agnostic product that you can use to create configurations for multiple cloud providers, including Amazon Web Services (AWS), Microsoft Azure, and Google Cloud Platform™ (GCP).
Cloud-agnostic means that you don’t need to create separate configurations for each provider. Create Terraform configurations using the HashiCorp Configuration Language (HCL). HCL is a declarative language that lets you define what your code must do through arguments and expressions.
The following code shows the basic elements used in Terraform files:
resource ” azurerm_linux_virtual_machine ” “main” {
cidr_block = var.base_cidr_block
}
<BLOCK TYPE> “<BLOCK LABEL>” “<BLOCK LABEL>” {
# Block body
<IDENTIFIER> = <EXPRESSION> # Argument
}
The code comprises blocks containing the configuration for objects like resources. The configuration includes a block type, optional labels, and a body that may contain nested blocks and arguments. The code also contains arguments to assign a value to a name and expressions representing or referencing values.
Terraform configuration file deployment happens in three steps: initialization, reviewing, and executing the file. To initialize a folder containing terraform configuration files, run terraform init.
Your resource configuration’s metadata is stored in a state file. While it is not always the case, a Terraform configuration can specify a backend block to determine where to store the state. Backend blocks are defined at the top level of a Terraform block. However, if your configuration file does not include a backend block, Terraform uses the default backend, which stores the state file locally as plain text. So, make sure to create a secure storage account for your state file.
After initialization, create an execution plan by running terraform plan. This command lets you review whether the results from executing the configuration file will match expectations. If the results meet expectations, run terraform apply to implement the changes.
Syntax
ARM templates are JSON code, and Terraform uses HCL. HCL is simple and is relatively easy to understand compared to JSON. HashiCorp designed HCL to handle infrastructure configuration code, unlike JSON. To understand the difference between HCL and JSON, let’s consider the following code block:
resource “azurerm_linux_virtual_machine” “myterraformvm” {
name = “simpleLinuxVM”
location = “eastus”
resource_group_name = azurerm_resource_group.myterraformgroup.name
network_interface_ids =[azurerm_network_interface.myterraformnic.id]
size = “Standard_DS1_v2”
os_disk {
name = “myOsDisk”
caching = “ReadWrite”
storage_account_type = “Standard_LRS”
}
source_image_reference {
publisher = “Canonical”
offer = “UbuntuServer”
sku = “18.04-LTS”
version = “latest”
}
computer_name = “simpleLinuxvm”
admin_username = “adminUser”
disable_password_authentication = true
admin_ssh_key {
username = “adminUser”
public_key = tls_private_key.example_ssh.public_key_openssh
}
boot_diagnostics {
storage_account_uri = azurerm_storage_account.mystorageaccount.primary_blob_endpoint
}
}
This code deploys a Linux VM, like the JSON file example of the ARM template. The code doesn’t include the complexity of ARM templates and doesn’t define parameters, variables, functions, or outputs. Instead, it provides concise code that’s easy to follow.
For instance, with Terraform, you don’t specify the type in the resource header, and you don’t need to reference the API version. Without the syntactic overhead, Terraform configuration files are easy to document and maintain, so tracking down errors or security bugs is less painful.
Modularity
Modules allow you to reuse configuration files in other deployment environments. Modularize the configuration code for ARM templates by nesting templates by splitting your template into multiple template files.
To reuse a template, you only need to reference it in the JSON file in which you want to use it. You can directly reference files located on a local machine or a remote source when using Terraform. This is different from ARM templates, which require referenced templates to be accessible within Azure. This means that you must deploy the linked template first before deploying the actual configuration files.
Storing the ARM template and its linked templates at an accessible endpoint can be insecure. Use the template spec to maintain the template as a resource and use Azure’s role-based access control (RBAC) to limit which users can deploy it.
Planning
With the terraform plan command, you can perform dry runs of your code, letting you know what the deployment changes. It’s sort of like a preview before making the actual changes.
ARM provides the same functionality using the what-if operation. This operation highlights the changes that will happen to the existing resource if you deploy the template.
These previews help you keep your code secure by ensuring code changes will not break vital functions.
Credentials
Terraform templates store credentials in plain-text state files. You should therefore encrypt the state file to protect the credentials from exposure.
There are no state files in Azure. Instead, you can extract credentials from the Azure Key Vault and referenced them inside the template.
If security is a concern, you might choose ARM or implement additional workarounds for Terraform templates like storing credentials in Key Vault then deleting the state file.
Access Management
ARM has RBAC and activity logs to help with security management. With RBAC, you can control who has the rights to access, modify, destroy, or deploy ARM templates. Then, using the activity logs, you can easily track who caused changes. Since Terraform is built on ARM, it has the same functions. However, Azure’s RBAC will first have to authorize your Terraform template.
ARM templates offer threat management and access control through RBAC and Azure’s activity log. Although Terraform can use these features with authentication, when managing on-premises resources, you do not get access to RBAC or the logging features. Even if a user modified the state file, there will be no record of that activity. Therefore, ARM templates provide a more reliable solution for user control and access management.
Cleaning Up
You can run the terraform destroy command to destroy resources in Terraform. This command is helpful for broken resources or those you created for testing purposes.
An ARM template doesn’t have an explicit destroy command. You need to use the Azure CLI or the Azure Portal to manually remove resources.
Removing these unneeded resources helps keep your infrastructure secure by shrinking your attack surface.
The Pros and Cons of ARM and Terraform
As IaC tools, both Terraform and ARM templates have helpful features. To choose between the two, consider your project’s needs and features.
For instance, Terraform stores credentials in plain text in the state file, while ARM templates do not have a state file. If you are an engineer concerned with securing your configuration files, you might choose ARM templates over Terraform. You could also use Terraform but implement additional workarounds for securing credentials, such as storing them in a Key Vault and deleting the state file.
If you want to integrate one cloud provider’s functions with another, a cloud agnostic tool like Terraform might be a good fit. Terraform’s agnosticism enables you to use the same cloud configuration to manage resources with different providers, plus you don’t have to learn multiple languages for each cloud provider.
ARM templates also use new Azure features as soon as they are released without needing any code changes since ARM templates are a native product. With open source Terraform, this takes time, since you must first configure Terraform to recognize the new changes in Azure. This delay can be an issue if your product needs the latest Azure features or if it is missing security-related updates.
Conclusion
When it comes to IaC tools, the choice depends on the needs of an application. If an application’s infrastructure must span multiple cloud providers, the best option is probably Terraform.
Now that you’ve seen a detailed explanation of the difference between ARM templates and Terraform and the pros and cons of each, you can make an informed choice about which is best for you.
Tool choice is just one of many considerations when switching your organization to IaC. Watch our Infrastructure as Code Explained video to learn more.